/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * OpexObjectLocationsReader.java
 * Copyright (C) 2021 University of Waikato, Hamilton, New Zealand
 */

package adams.data.io.input;

import adams.core.io.FileUtils;
import adams.core.io.PlaceholderFile;
import adams.data.io.output.OpexObjectLocationsWriter;
import adams.data.report.DataType;
import adams.data.report.Field;
import adams.data.report.Report;
import adams.env.Environment;
import adams.flow.transformer.locateobjects.LocatedObject;
import adams.flow.transformer.locateobjects.LocatedObjects;
import adams.flow.transformer.locateobjects.ObjectPrefixHandler;
import gnu.trove.list.TIntList;
import gnu.trove.list.array.TIntArrayList;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import net.minidev.json.parser.JSONParser;

import java.awt.*;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

/**
 <!-- globalinfo-start -->
 * Reads object locations from JSON generated by the opex library.<br>
 * <br>
 * See more:<br>
 * https:&#47;&#47;github.com&#47;WaikatoLink2020&#47;objdet-predictions-exchange-format
 * <br><br>
 <!-- globalinfo-end -->
 *
 <!-- options-start -->
 * <pre>-logging-level &lt;OFF|SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST&gt; (property: loggingLevel)
 * &nbsp;&nbsp;&nbsp;The logging level for outputting errors and debugging output.
 * &nbsp;&nbsp;&nbsp;default: WARNING
 * </pre>
 *
 * <pre>-input &lt;adams.core.io.PlaceholderFile&gt; (property: input)
 * &nbsp;&nbsp;&nbsp;The file to read and turn into a report.
 * &nbsp;&nbsp;&nbsp;default: ${CWD}
 * </pre>
 *
 * <pre>-id &lt;adams.data.report.Field&gt; (property: ID)
 * &nbsp;&nbsp;&nbsp;The field to use for storing the ID.
 * &nbsp;&nbsp;&nbsp;default: ID[S]
 * </pre>
 *
 * <pre>-timestamp &lt;adams.data.report.Field&gt; (property: timestamp)
 * &nbsp;&nbsp;&nbsp;The field to use for storing the timestamp.
 * &nbsp;&nbsp;&nbsp;default: Timestamp[S]
 * </pre>
 *
 * <pre>-prefix &lt;java.lang.String&gt; (property: prefix)
 * &nbsp;&nbsp;&nbsp;The report field prefix used in the report for the objects.
 * &nbsp;&nbsp;&nbsp;default: Object.
 * </pre>
 *
 * <pre>-label-suffix &lt;java.lang.String&gt; (property: labelSuffix)
 * &nbsp;&nbsp;&nbsp;The suffix to use in the report for labels.
 * &nbsp;&nbsp;&nbsp;default: type
 * </pre>
 *
 * <pre>-score-suffix &lt;java.lang.String&gt; (property: scoreSuffix)
 * &nbsp;&nbsp;&nbsp;The suffix to use in the report for scores.
 * &nbsp;&nbsp;&nbsp;default: score
 * </pre>
 *
 * <pre>-meta-prefix &lt;java.lang.String&gt; (property: metaPrefix)
 * &nbsp;&nbsp;&nbsp;The report field prefix used in the report for the meta-data.
 * &nbsp;&nbsp;&nbsp;default: Meta.
 * </pre>
 *
 <!-- options-end -->
 *
 * @author fracpete (fracpete at waikato dot ac dot nz)
 */
public class OpexObjectLocationsReader
    extends AbstractReportReader<Report>
    implements ObjectPrefixHandler {

  /** the field to use for the ID. */
  protected Field m_ID;

  /** the field to use for the timestamp. */
  protected Field m_Timestamp;

  /** the prefix to use for objects. */
  protected String m_Prefix;

  /** the label suffix to use. */
  protected String m_LabelSuffix;

  /** the score suffix to use. */
  protected String m_ScoreSuffix;

  /** the prefix for the meta-data. */
  protected String m_MetaPrefix;

  /**
   * Returns a string describing the object.
   *
   * @return 			a description suitable for displaying in the gui
   */
  @Override
  public String globalInfo() {
    return "Reads object locations from JSON generated by the opex library.\n\n"
	+ "See more:\n"
	+ "https://github.com/WaikatoLink2020/objdet-predictions-exchange-format";
  }

  /**
   * Adds options to the internal list of options.
   */
  @Override
  public void defineOptions() {
    super.defineOptions();

    m_OptionManager.add(
	"id", "ID",
	new Field("ID", DataType.STRING));

    m_OptionManager.add(
	"timestamp", "timestamp",
	new Field("Timestamp", DataType.STRING));

    m_OptionManager.add(
	"prefix", "prefix",
	"Object.");

    m_OptionManager.add(
	"label-suffix", "labelSuffix",
	"type");

    m_OptionManager.add(
	"score-suffix", "scoreSuffix",
	"score");

    m_OptionManager.add(
	"meta-prefix", "metaPrefix",
	"Meta.");
  }

  /**
   * Sets the field to use for the ID.
   *
   * @param value 	the field
   */
  public void setID(Field value) {
    m_ID = value;
    reset();
  }

  /**
   * Returns the field to use for the ID.
   *
   * @return 		the field
   */
  public Field getID() {
    return m_ID;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String IDTipText() {
    return "The field to use for storing the ID.";
  }

  /**
   * Sets the field to use for the timestamp.
   *
   * @param value 	the field
   */
  public void setTimestamp(Field value) {
    m_Timestamp = value;
    reset();
  }

  /**
   * Returns the field to use for the timestamp.
   *
   * @return 		the field
   */
  public Field getTimestamp() {
    return m_Timestamp;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String timestampTipText() {
    return "The field to use for storing the timestamp.";
  }

  /**
   * Sets the field prefix used in the report for the objects.
   *
   * @param value 	the field prefix
   */
  @Override
  public void setPrefix(String value) {
    m_Prefix = value;
    reset();
  }

  /**
   * Returns the field prefix used in the report for the objects.
   *
   * @return 		the field prefix
   */
  @Override
  public String getPrefix() {
    return m_Prefix;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  @Override
  public String prefixTipText() {
    return "The report field prefix used in the report for the objects.";
  }

  /**
   * Sets the field suffix used in the report for labels.
   *
   * @param value 	the field suffix
   */
  public void setLabelSuffix(String value) {
    m_LabelSuffix = value;
    reset();
  }

  /**
   * Returns the field suffix used in the report for labels.
   *
   * @return 		the field suffix
   */
  public String getLabelSuffix() {
    return m_LabelSuffix;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String labelSuffixTipText() {
    return "The suffix to use in the report for labels.";
  }

  /**
   * Sets the field suffix used in the report for scores.
   *
   * @param value 	the field suffix
   */
  public void setScoreSuffix(String value) {
    m_ScoreSuffix = value;
    reset();
  }

  /**
   * Returns the field suffix used in the report for scores.
   *
   * @return 		the field suffix
   */
  public String getScoreSuffix() {
    return m_ScoreSuffix;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String scoreSuffixTipText() {
    return "The suffix to use in the report for scores.";
  }

  /**
   * Sets the field prefix used in the report for the meta-data.
   *
   * @param value 	the field prefix
   */
  public void setMetaPrefix(String value) {
    m_MetaPrefix = value;
    reset();
  }

  /**
   * Returns the field prefix used in the report for the meta-data.
   *
   * @return 		the field prefix
   */
  public String getMetaPrefix() {
    return m_MetaPrefix;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String metaPrefixTipText() {
    return "The report field prefix used in the report for the meta-data.";
  }

  /**
   * Returns a string describing the format (used in the file chooser).
   *
   * @return 			a description suitable for displaying in the
   * 				file chooser
   */
  @Override
  public String getFormatDescription() {
    return "Opex annotations";
  }

  /**
   * Returns the extension(s) of the format.
   *
   * @return 			the extension(s) (without the dot!)
   */
  @Override
  public String[] getFormatExtensions() {
    return new String[]{"json"};
  }

  /**
   * Tries to determine the parent ID for the current report.
   *
   * @param report	the report to determine the ID for
   * @return		the parent database ID, -1 if it cannot be determined
   */
  @Override
  protected int determineParentID(Report report) {
    return -1;
  }

  /**
   * Returns a new instance of the report class in use.
   *
   * @return		the new (empty) report
   */
  @Override
  public Report newInstance() {
    return new Report();
  }

  /**
   * Performs the actual reading.
   *
   * @return		the reports that were read
   */
  @Override
  protected List<Report> readData() {
    List<Report> 	result;
    JSONParser 		parser;
    FileReader 		freader;
    BufferedReader 	breader;
    LocatedObject 	lobj;
    LocatedObjects 	lobjs;
    JSONObject 		obj;
    JSONObject 		meta;
    JSONArray		objects;
    int			i;
    JSONObject		object;
    JSONObject		bbox;
    JSONArray 		points;
    JSONArray		pair;
    int			n;
    Double		score;
    String		label;
    int			left;
    int			right;
    int			top;
    int			bottom;
    TIntList		xPoints;
    TIntList		yPoints;

    Report		report;

    result  = new ArrayList<>();
    freader = null;
    breader = null;
    try {
      freader = new FileReader(m_Input.getAbsolutePath());
      breader = new BufferedReader(freader);
      parser  = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
      obj     = (JSONObject) parser.parse(breader);
      report  = newInstance();
      report.addField(m_ID);
      report.addField(m_Timestamp);

      // meta-data
      if (obj.containsKey("timestamp"))
	report.setStringValue(m_Timestamp.getName(), obj.getAsString("timestamp"));
      if (obj.containsKey("id"))
	report.setStringValue(m_ID.getName(), obj.getAsString("id"));
      if (obj.containsKey("meta")) {
	meta = (JSONObject) obj.get("meta");
	for (String key: meta.keySet())
	  report.setStringValue(m_MetaPrefix + key, "" + meta.get(key));
      }

      // objects
      lobjs = new LocatedObjects();
      if (obj.containsKey("objects")) {
	objects = (JSONArray) obj.get("objects");
	xPoints = new TIntArrayList();
	yPoints = new TIntArrayList();
	for (i = 0; i < objects.size(); i++) {
	  object = (JSONObject) objects.get(i);
	  // score
	  score = null;
	  if (object.containsKey("score"))
	    score = object.getAsNumber("score").doubleValue();
	  // label
	  label = null;
	  if (object.containsKey("label"))
	    label = object.getAsString("label");
	  // bbox
	  bbox   = (JSONObject) object.get("bbox");
	  left   = bbox.getAsNumber("left").intValue();
	  top    = bbox.getAsNumber("top").intValue();
	  right  = bbox.getAsNumber("right").intValue();
	  bottom = bbox.getAsNumber("bottom").intValue();
	  // polygon
	  xPoints.clear();
	  yPoints.clear();
	  points = (JSONArray) ((JSONObject) object.get("polygon")).get("points");
	  for (n = 0; n < points.size(); n++) {
	    pair = (JSONArray) points.get(n);
	    xPoints.add(((Number) pair.get(0)).intValue());
	    yPoints.add(((Number) pair.get(1)).intValue());
	  }
	  // create located object
	  lobj = new LocatedObject(left, top, right-left+1, bottom-top+1);
	  lobj.setPolygon(new Polygon(xPoints.toArray(), yPoints.toArray(), xPoints.size()));
	  if (score != null)
	    lobj.getMetaData().put(m_ScoreSuffix, score);
	  if (label != null)
	    lobj.getMetaData().put(m_LabelSuffix, label);
	  lobjs.add(lobj);
	}
      }
      report.mergeWith(lobjs.toReport(m_Prefix));
      result.add(report);
    }
    catch (Exception e) {
      getLogger().log(Level.SEVERE, "Failed to read JSON file: " + m_Input, e);
    }
    finally {
      FileUtils.closeQuietly(breader);
      FileUtils.closeQuietly(freader);
    }

    return result;
  }
}
